package clases;

import java.util.ArrayList;
import java.util.Collections;

public class Population {
  public static final int MAX_HEIGHT = 15;
  public static final int ELITE_MAX = (int)Math.ceil(GeneticAlgorithm.K*0.1);
  public int popSize;
  public Individuo best;
  public double meanAptitude;
  public double totalAptitude;
  public ArrayList<Individuo> individuos;
  public String solution;
  public int solution_size;

  public Population() {
    this.best = null;
    this.meanAptitude = 0.0;
    this.totalAptitude = 0.0;
    this.popSize = 0;
    this.individuos = new ArrayList<Individuo>();
    this.solution = null;
    this.solution_size = 32;
  }

  public Population(int size, String sol) {
    this.best = null;
    this.meanAptitude = 0.0;
    this.totalAptitude = 0.0;
    this.popSize = size;
    this.individuos = new ArrayList<Individuo>(size);
    this.solution = new String(sol);
    this.solution_size = sol.length();
  }

  public void randomPopulation() {
    for (int i = 0; i < this.popSize; i++) {
      Individuo circuit = new Individuo(this.solution, this.solution_size);
      circuit.randomIndividuo(Population.MAX_HEIGHT);
      this.individuos.add(i, circuit);
    }
  }

  public Population selection(int k, String method) throws Exception {
    Population resp = new Population();
    this.calculateAptitudes();
Collections.shuffle(this.individuos);
    // System.out.println("selection. popsize= " + this.popSize + "--" +
    // this.getSize());

    // ELITE, RULETA, UNIVERSAL, ELITE+RULETA, ELITE+UNIVERSAL
    if (method.equals("ELITE"))
      // resp.individuos = this.selectionElite(k);
      resp.individuos = this.selectionElite2(k);
    else if (method.equals("RULETA"))
      resp.individuos = this.selectionRuleta(k);
    else if (method.equals("UNIVERSAL"))
      resp.individuos = this.selectionUniversal(k);
    else if (method.equals("ELITE+RULETA"))
      resp.individuos = this.selectionEliteRuleta(Population.ELITE_MAX, k);
    else
      // "ELITE+UNIVERSAL"
      resp.individuos = this.selectionEliteUniversal(Population.ELITE_MAX, k);
    resp.popSize = resp.individuos.size();

    // System.out.println("selection. popsize= " + this.popSize + "--" +
    // this.getSize());
    // System.out.println("selection. resp= " + resp.popSize + "--" +
    // resp.getSize());
    return resp;
  }

  private ArrayList<Individuo> selectionEliteUniversal(int n, int k)
      throws Exception {

    if (n > k) {
      return null;
    }
    ArrayList<Individuo> eliteIndividuals = this.selectionElite(n);
    ArrayList<Individuo> universalIndividuals;
    universalIndividuals = this.selectionUniversal(k - n);

    ArrayList<Individuo> result = new ArrayList<Individuo>(eliteIndividuals);
    result.addAll(universalIndividuals);
    return result;

  }

  private ArrayList<Individuo> selectionElite2(int k) throws Exception {
    ArrayList<Individuo> candidatos = new ArrayList<Individuo>(k);
    double min = this.individuos.get(0).getAptitude();
    int minidx;
    int i;
    Boolean existe;

    // pongo el primero como candidato inicial para empezar
    candidatos.add(0, this.individuos.get(0));
    // lo seteo como el minimo por ahora
    min = candidatos.get(0).getAptitude();
    minidx = 0;
    i=1;
    // mientras que no agreguemos los k mejores y no lleguemos al final del
    // array
    while(i<this.getSize()){
      // si el nuevo individuo es mejor y no esta repetido, lo agrego a
      // candidats
      if(this.individuos.get(i).getAptitude() >= min){
        existe = false;
        for(int j=0; j<candidatos.size() && !existe; j++){
          if(candidatos.get(j).getOutput().equals(this.individuos.get(i).getOutput()))
            existe=true;
        }
        if(!existe){
          if(candidatos.size()<k){
            candidatos.add(this.individuos.get(i).clone());
            existe=true;
          }else{
            candidatos.set(minidx, this.individuos.get(i).clone());
            existe=true;
          }
          for (int y = 1; y < candidatos.size(); y++){
            if (candidatos.get(y).getAptitude() < min){
              minidx = y;
              min = candidatos.get(y).getAptitude();
            }
          }
        }
      }
      i++;
    }
    //System.out.println("ANTES-candidatos: "+candidatos.size());
    if(candidatos.size() < k){   
      min = 0;//this.individuos.get(this.popSize-1).getAptitude();
      i=this.getSize()-1;
      while(i>=0 && candidatos.size()<k){
          // si el nuevo individuo es mejor y no esta repetido, lo agrego a
          // candidats
          if(this.individuos.get(i).getAptitude() >= min){
            candidatos.add(this.individuos.get(i).clone());
            for (int y = 0; y < candidatos.size(); y++){
              if (candidatos.get(y).getAptitude() < min){
                min = candidatos.get(y).getAptitude();
              }
            }
          }
        i--;
      }
    }
    //System.out.println("DESPUES-candidatos: "+ candidatos.size());
    return candidatos;
  }

  private ArrayList<Individuo> selectionEliteRuleta(int n, int k)
      throws Exception {
    if (n > k) {
      return null;
    }
    ArrayList<Individuo> eliteIndividuals = this.selectionElite(n);
    ArrayList<Individuo> ruletaIndividuals;
    ruletaIndividuals = this.selectionRuleta(k - n);

    ArrayList<Individuo> result = new ArrayList<Individuo>(eliteIndividuals);
    result.addAll(ruletaIndividuals);
    return result;
  }

  private ArrayList<Individuo> selectionUniversal(int k) {
    int i = 0;
    ArrayList<Individuo> result = new ArrayList<Individuo>(k);

    for (int j = 0; j < k; j++) {
      double rand = (Math.random() + j - 1) / k;

      for (i = 0; i < this.individuos.size(); i++) {
        double apt = this.individuos.get(i).getAccumulatedAptitude();
        if (apt > rand) {
          result.add(this.individuos.get(i));
          break;
        }
      }
    }

    return result;
  }

  private ArrayList<Individuo> selectionRuleta(int k) {

    int i = 0;
    ArrayList<Individuo> result = new ArrayList<Individuo>(k);

    for (int j = 0; j < k; j++) {
      double rand = Math.random();

      for (i = 0; i < this.individuos.size(); i++) {
        double apt = this.individuos.get(i).getAccumulatedAptitude();
        if (apt > rand) {
          result.add(this.individuos.get(i));
          break;
        }
      }
    }

    return result;
  }

  private ArrayList<Individuo> selectionElite(int k) throws Exception {
    ArrayList<Individuo> candidatos = new ArrayList<Individuo>(k);
    double min = this.individuos.get(0).getAptitude();
    int minidx = 0;

    // inicializo los candidatos
    for (int i = 0; i < k; i++) {
      candidatos.add(i, this.individuos.get(i).clone());
      if (this.individuos.get(i).getAptitude() < min) {
        minidx = i;
        min = this.individuos.get(i).getAptitude();
      }
    }
    for (int i = k; i < this.getSize(); i++) {
      // si el proximo individuo tiene mejor aptitud que la minima que yo tengo
      // lo cambio
      if (this.individuos.get(i).getAptitude() > min) {
        candidatos.set(minidx, this.individuos.get(i).clone());
        min = candidatos.get(0).getAptitude();
        minidx = 0;
        // recorro candidatos para encontrar el candidato de menor aptitud
        for (int j = 1; j < k; j++) {
          if (candidatos.get(j).getAptitude() < min) {
            minidx = j;
            min = candidatos.get(j).getAptitude();
          }
        }
      }
    }
    return candidatos;
  }

  private void calculateAptitudes() throws Exception {
    this.meanAptitude = 0.0;
    this.totalAptitude = 0.0;
    this.best = this.individuos.get(0);
    for (int i = 0; i < this.popSize; i++) {
      double apt = this.individuos.get(i).getAptitude();
      this.totalAptitude += apt;
      if (apt > this.best.getAptitude()) {
        this.best = this.individuos.get(i);
      }
    }
    this.meanAptitude = (this.totalAptitude / this.popSize);

    double acum = 0;
    for (int i = 0; i < this.popSize; i++) {
      double relative = this.individuos.get(i).getAptitude()
          / this.totalAptitude;
      acum += relative;
      this.individuos.get(i).setRelativeAptitude(relative);
      this.individuos.get(i).setAccumulatedAptitude(acum);
    }
  }

  public Population reproduction() {
    Population resp = new Population();
    for (int i = 0; i < this.popSize; i += 2) {
      this.individuos.get(i).reproduction(this.individuos.get(i + 1));
      resp.individuos.add(i, this.individuos.get(i)); // hijo 1
      resp.individuos.add(i + 1, this.individuos.get(i + 1)); // hijo 2
    }
    resp.popSize = resp.individuos.size();
    return resp;
  }

  public void replace(Population offsprings, String replacement)
      throws Exception {
    // ELITE, RULETA, UNIVERSAL, ELITE+RULETA, ELITE+UNIVERSAL
    this.individuos.addAll(offsprings.individuos);
    this.popSize = this.getSize();
    this.calculateAptitudes();

    if (replacement.equals("ELITE"))
      this.individuos = this.selectionElite2(GeneticAlgorithm.POP_SIZE);
    else if (replacement.equals("RULETA"))
      this.individuos = this.selectionRuleta(GeneticAlgorithm.POP_SIZE);
    else if (replacement.equals("UNIVERSAL"))
      this.individuos = this.selectionUniversal(GeneticAlgorithm.POP_SIZE);
    else if (replacement.equals("ELITE+RULETA"))
      this.individuos = this.selectionEliteRuleta(ELITE_MAX,
          GeneticAlgorithm.POP_SIZE);
    else
      this.individuos = this.selectionEliteUniversal(ELITE_MAX,
          GeneticAlgorithm.POP_SIZE);
    this.popSize = this.individuos.size();
    this.calculateAptitudes();
  }

  public void mutarPoblacion(Population nueva) {
    //System.out.println("nuevos size:"+nueva.getSize());
    for(int i=nueva.getSize()-1; i>=0; i-=2){
      this.individuos.set(i, nueva.individuos.get(i));
    }
  }

  public int getSize() {
    return this.individuos.size();
  }

  public void printBest() throws Exception {
    this.best.printOutput();
  }
}
